// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/gl/gl_context_cgl.h"

#include <OpenGL/CGLRenderers.h>
#include <OpenGL/CGLTypes.h>
#include <vector>

#include "base/debug/trace_event.h"
#include "base/logging.h"
#include "ui/gl/gl_bindings.h"
#include "ui/gl/gl_implementation.h"
#include "ui/gl/gl_surface_cgl.h"
#include "ui/gl/gpu_switching_manager.h"

namespace gfx {

bool g_support_renderer_switching;

static CGLPixelFormatObj GetPixelFormat() {
  static CGLPixelFormatObj format;
  if (format)
    return format;
  std::vector<CGLPixelFormatAttribute> attribs;
  // If the system supports dual gpus then allow offline renderers for every
  // context, so that they can all be in the same share group.
  if (ui::GpuSwitchingManager::GetInstance()->SupportsDualGpus()) {
    attribs.push_back(kCGLPFAAllowOfflineRenderers);
    g_support_renderer_switching = true;
  }
  if (GetGLImplementation() == kGLImplementationAppleGL) {
    attribs.push_back(kCGLPFARendererID);
    attribs.push_back((CGLPixelFormatAttribute) kCGLRendererGenericFloatID);
    g_support_renderer_switching = false;
  }
  attribs.push_back((CGLPixelFormatAttribute) 0);

  GLint num_virtual_screens;
  if (CGLChoosePixelFormat(&attribs.front(),
                           &format,
                           &num_virtual_screens) != kCGLNoError) {
    LOG(ERROR) << "Error choosing pixel format.";
    return NULL;
  }
  if (!format) {
    LOG(ERROR) << "format == 0.";
    return NULL;
  }
  DCHECK_NE(num_virtual_screens, 0);
  return format;
}

GLContextCGL::GLContextCGL(GLShareGroup* share_group)
  : GLContext(share_group),
    context_(NULL),
    gpu_preference_(PreferIntegratedGpu),
    discrete_pixelformat_(NULL),
    screen_(-1),
    renderer_id_(-1) {
}

bool GLContextCGL::Initialize(GLSurface* compatible_surface,
                              GpuPreference gpu_preference) {
  DCHECK(compatible_surface);

  gpu_preference = ui::GpuSwitchingManager::GetInstance()->AdjustGpuPreference(
      gpu_preference);

  GLContextCGL* share_context = share_group() ?
      static_cast<GLContextCGL*>(share_group()->GetContext()) : NULL;

  CGLPixelFormatObj format = GetPixelFormat();
  if (!format)
    return false;

  // If using the discrete gpu, create a pixel format requiring it before we
  // create the context.
  if (!ui::GpuSwitchingManager::GetInstance()->SupportsDualGpus() ||
      gpu_preference == PreferDiscreteGpu) {
    std::vector<CGLPixelFormatAttribute> discrete_attribs;
    discrete_attribs.push_back((CGLPixelFormatAttribute) 0);
    GLint num_pixel_formats;
    if (CGLChoosePixelFormat(&discrete_attribs.front(),
                             &discrete_pixelformat_,
                             &num_pixel_formats) != kCGLNoError) {
      LOG(ERROR) << "Error choosing pixel format.";
      return false;
    }
    // The renderer might be switched after this, so ignore the saved ID.
    share_group()->SetRendererID(-1);
  }

  CGLError res = CGLCreateContext(
      format,
      share_context ?
          static_cast<CGLContextObj>(share_context->GetHandle()) : NULL,
      reinterpret_cast<CGLContextObj*>(&context_));
  if (res != kCGLNoError) {
    LOG(ERROR) << "Error creating context.";
    Destroy();
    return false;
  }

  gpu_preference_ = gpu_preference;
  return true;
}

void GLContextCGL::Destroy() {
  if (discrete_pixelformat_) {
    // Delay releasing the pixel format for 10 seconds to reduce the number of
    // unnecessary GPU switches.
    MessageLoop::current()->PostDelayedTask(FROM_HERE,
                                            base::Bind(&CGLReleasePixelFormat,
                                                       discrete_pixelformat_),
                                            base::TimeDelta::FromSeconds(10));
    discrete_pixelformat_ = NULL;
  }
  if (context_) {
    CGLDestroyContext(static_cast<CGLContextObj>(context_));
    context_ = NULL;
  }
}

bool GLContextCGL::MakeCurrent(GLSurface* surface) {
  DCHECK(context_);
  int renderer_id = share_group()->GetRendererID();
  int screen;
  CGLGetVirtualScreen(static_cast<CGLContextObj>(context_), &screen);

  if (g_support_renderer_switching &&
      !discrete_pixelformat_ && renderer_id != -1 &&
      (screen != screen_ || renderer_id != renderer_id_)) {
    // Attempt to find a virtual screen that's using the requested renderer,
    // and switch the context to use that screen. Don't attempt to switch if
    // the context requires the discrete GPU.
    CGLPixelFormatObj format = GetPixelFormat();
    int virtual_screen_count;
    if (CGLDescribePixelFormat(format, 0, kCGLPFAVirtualScreenCount,
                               &virtual_screen_count) != kCGLNoError)
      return false;

    for (int i = 0; i < virtual_screen_count; ++i) {
      int screen_renderer_id;
      if (CGLDescribePixelFormat(format, i, kCGLPFARendererID,
                                 &screen_renderer_id) != kCGLNoError)
        return false;

      screen_renderer_id &= kCGLRendererIDMatchingMask;
      if (screen_renderer_id == renderer_id) {
        CGLSetVirtualScreen(static_cast<CGLContextObj>(context_), i);
        screen_ = i;
        break;
      }
    }
    renderer_id_ = renderer_id;
  }

  if (IsCurrent(surface))
    return true;

  TRACE_EVENT0("gpu", "GLContextCGL::MakeCurrent");

  if (CGLSetCurrentContext(
      static_cast<CGLContextObj>(context_)) != kCGLNoError) {
    LOG(ERROR) << "Unable to make gl context current.";
    return false;
  }

  SetCurrent(this, surface);
  if (!InitializeExtensionBindings()) {
    ReleaseCurrent(surface);
    return false;
  }

  if (!surface->OnMakeCurrent(this)) {
    LOG(ERROR) << "Unable to make gl context current.";
    return false;
  }

  SetRealGLApi();
  return true;
}

void GLContextCGL::ReleaseCurrent(GLSurface* surface) {
  if (!IsCurrent(surface))
    return;

  SetCurrent(NULL, NULL);
  CGLSetCurrentContext(NULL);
}

bool GLContextCGL::IsCurrent(GLSurface* surface) {
  bool native_context_is_current = CGLGetCurrentContext() == context_;

  // If our context is current then our notion of which GLContext is
  // current must be correct. On the other hand, third-party code
  // using OpenGL might change the current context.
  DCHECK(!native_context_is_current || (GetCurrent() == this));

  if (!native_context_is_current)
    return false;

  return true;
}

void* GLContextCGL::GetHandle() {
  return context_;
}

void GLContextCGL::SetSwapInterval(int interval) {
  DCHECK(IsCurrent(NULL));
  LOG(WARNING) << "GLContex: GLContextCGL::SetSwapInterval is ignored.";
}


bool GLContextCGL::GetTotalGpuMemory(size_t* bytes) {
  DCHECK(bytes);
  *bytes = 0;

  CGLContextObj context = reinterpret_cast<CGLContextObj>(context_);
  if (!context)
    return false;

  // Retrieve the current renderer ID
  GLint current_renderer_id = 0;
  if (CGLGetParameter(context,
                      kCGLCPCurrentRendererID,
                      &current_renderer_id) != kCGLNoError)
    return false;

  // Iterate through the list of all renderers
  GLuint display_mask = static_cast<GLuint>(-1);
  CGLRendererInfoObj renderer_info = NULL;
  GLint num_renderers = 0;
  if (CGLQueryRendererInfo(display_mask,
                           &renderer_info,
                           &num_renderers) != kCGLNoError)
    return false;

  ScopedCGLRendererInfoObj scoper(renderer_info);

  for (GLint renderer_index = 0;
       renderer_index < num_renderers;
       ++renderer_index) {
    // Skip this if this renderer is not the current renderer.
    GLint renderer_id = 0;
    if (CGLDescribeRenderer(renderer_info,
                            renderer_index,
                            kCGLRPRendererID,
                            &renderer_id) != kCGLNoError)
        continue;
    if (renderer_id != current_renderer_id)
        continue;
    // Retrieve the video memory for the renderer.
    GLint video_memory = 0;
    if (CGLDescribeRenderer(renderer_info,
                            renderer_index,
                            kCGLRPVideoMemory,
                            &video_memory) != kCGLNoError)
        continue;
    *bytes = video_memory;
    return true;
  }

  return false;
}

GLContextCGL::~GLContextCGL() {
  Destroy();
}

GpuPreference GLContextCGL::GetGpuPreference() {
  return gpu_preference_;
}

void ScopedCGLDestroyRendererInfo::operator()(CGLRendererInfoObj x) const {
  CGLDestroyRendererInfo(x);
}

}  // namespace gfx
